Skip to content

Read a root file a document under docs/ links, which neither reading reached - #169

Merged
iderex merged 1 commit into
mainfrom
invariants/a-document-under-docs-linking-a-root-file
Aug 22, 2026
Merged

Read a root file a document under docs/ links, which neither reading reached#169
iderex merged 1 commit into
mainfrom
invariants/a-document-under-docs-linking-a-root-file

Conversation

@iderex

@iderex iderex commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Closes #168

What this changes

A document under docs/ writes a link to a file at the repository root as
../NAME, and that target was read by neither of the two readings the paths leg
joins. PathsNamedInProse requires a leading segment naming a directory of this
tree and .. is not one; LinkTargetsWithoutADirectory skips any target
carrying a slash. The two sat either side of the shape.

Neither of them grows. Both are exact about a population and argue for that
exactness in their own comments, so this adds a third reading,
LinkTargetsAboveTheirDocument, which returns a link target stepping out of the
document's own directory and leaves the caller to place it.

pathsNamedIn joins it to the document's own directory rather than to the root,
which is what the issue requires of whichever reading was chosen, and it drops a
target that climbs past the root of the checkout.

Two fixtures hold both directions, in the register the two neighbouring cases
already use: a document under docs/ linking a root file that is not there, and
its near neighbour linking one that is.

What failure it prevents

The passing run. Three links on the default branch are in this shape and all
three resolve, so nothing is broken today. The day one of them is not has
already happened once: LICENSE left the default branch for a day under #155
and docs/operator-guide.md would have pointed at nothing while the leg stayed
green.

What was run

At 2eb07c1, on windows/amd64.

The near-miss the issue names, in both spellings. Before this change the ../
spelling passed and only the bare spelling reddened. Both redden now:

printf '\n[a file that is not there](../NO-SUCH-FILE.md)\n' >> docs/privacy.md
go test ./internal/invariants -run TestThisRepositorySatisfiesTheInvariants -count=1 -v
          paths this repository's own documents name: 34 examined
          ..\..\docs\privacy.md: it names NO-SUCH-FILE.md, which is not in this tree (document-names-a-path-that-does-not-resolve)
--- FAIL: TestThisRepositorySatisfiesTheInvariants (0.15s)
printf '\n[a file that is not there](NO-SUCH-FILE.md)\n' >> docs/privacy.md
go test ./internal/invariants -run TestThisRepositorySatisfiesTheInvariants -count=1 -v
          paths this repository's own documents name: 34 examined
          ..\..\docs\privacy.md: it names docs/NO-SUCH-FILE.md, which is not in this tree (document-names-a-path-that-does-not-resolve)
--- FAIL: TestThisRepositorySatisfiesTheInvariants (0.15s)

The guard deleted, which is what says it bites for the reason it names. With the
third reading's loop taken out of pathsNamedIn and nothing else changed, one
case reddens and no other:

go test ./internal/invariants -run TestCases -count=1
--- FAIL: TestCases (0.02s)
    --- FAIL: TestCases/a-document-under-docs-linking-a-root-file-that-is-not-there (0.00s)
        invariants_test.go:159: expected refusal not produced: document-names-a-path-that-does-not-resolve
FAIL

The three links the issue lists are subjects the leg examines rather than text it
walks past. Moving LICENSE out of the tree and running the leg reaches both of
the docs/ ones, alongside the two root documents the second reading already
had:

mv LICENSE LICENCE-MOVED-FOR-A-MEASUREMENT
go test ./internal/invariants -run TestThisRepositorySatisfiesTheInvariants -count=1 -v
          ..\..\README.md: it names LICENSE, which is not in this tree (document-names-a-path-that-does-not-resolve)
          ..\..\SECURITY.md: it names LICENSE, which is not in this tree (document-names-a-path-that-does-not-resolve)
          ..\..\docs\operator-guide.md: it names LICENSE, which is not in this tree (document-names-a-path-that-does-not-resolve)
          ..\..\docs\promotion.md: it names LICENSE, which is not in this tree (document-names-a-path-that-does-not-resolve)

and the third one the same way:

mv NOTICE.md NOTICE-MOVED-FOR-A-MEASUREMENT.md
go test ./internal/invariants -run TestThisRepositorySatisfiesTheInvariants -count=1 -v
          ..\..\docs\operator-guide.md: it names NOTICE.md, which is not in this tree (document-names-a-path-that-does-not-resolve)
          ..\..\README.md: it names NOTICE.md, which is not in this tree (document-names-a-path-that-does-not-resolve)

Both files were put back before the commit, which git status reported clean
afterwards.

The gate CONTRIBUTING.md names, in the order it names it:

go build ./cmd/... ./internal/...
go vet ./cmd/... ./internal/...
gofmt -l cmd internal
go test -count=1 -v ./cmd/... ./internal/...

gofmt -l printed nothing, which is its passing result. The suite:

ok  	github.com/Flowfin/lab/cmd/contexts	1.354s
ok  	github.com/Flowfin/lab/cmd/lab	2.885s
ok  	github.com/Flowfin/lab/cmd/notices	10.550s
ok  	github.com/Flowfin/lab/cmd/pullrequest	0.845s
ok  	github.com/Flowfin/lab/internal/check	3.081s
ok  	github.com/Flowfin/lab/internal/contexts	1.142s
ok  	github.com/Flowfin/lab/internal/hardware	0.831s
ok  	github.com/Flowfin/lab/internal/invariants	1.590s
ok  	github.com/Flowfin/lab/internal/notices	1.132s
ok  	github.com/Flowfin/lab/internal/prose	1.044s
ok  	github.com/Flowfin/lab/internal/pullrequest	1.032s

and the runner over this tree:

go run ./cmd/lab check .
examined .
1 experiment directory walked, 1 record read
18 decision records read
the time this run read is 2026-08-22T07:45:01Z
0 refused

That run is on one platform. What the other two suite platforms do with this
change is not measured here and is the workflow's answer rather than mine.

What this does not do

Nobody but me has read this change. The evidence above is in place of a second
reader rather than alongside one, and it is a weaker thing.

It does not widen PathsNamedInProse to read .. in a sentence. That pattern is
narrow on purpose and the reason is written where it is narrow, so ../LICENSE
typed into a sentence is still a word here and only a link is resolved.

It says nothing about a target that climbs past the root of the checkout. Such a
target is returned by the reading and dropped by the caller, deliberately and
with the reason at the drop: whether a file outside the checkout exists is not a
question any reading of this tree answers, which is the same reason a target
carrying a scheme is left alone. So a document linking ../../somewhere is
neither resolved nor refused, and a run that is green says nothing about it.

The fixture proves the property and never which line inside the leg produced it,
which is the bound the harness states about itself. The arms a tree cannot reach
are covered by the direct table in internal/check/paths_test.go instead: a
fragment on the end, a repeated target, ordering, a scheme, a backslash, and the
two shapes that belong to the other two readings.

…reached

A document under docs/ writes a link to a file at the root as ../NAME, and that
target fell between the two readings the paths leg joins. PathsNamedInProse
requires a leading segment naming a directory of this tree and two full stops
are not one; LinkTargetsWithoutADirectory skips any target carrying a slash.
So a document under docs/ could link a root file that is not in the tree and
nothing said so, which is the case #162 raised and #165 closed one directory up.

The failure this prevents is the passing run. Three links on the default branch
are in that shape today and all three resolve, so nothing is broken until the
day one of them is not, and that day has already happened once: LICENSE left
the default branch for a day and docs/operator-guide.md would have pointed at
nothing while the leg stayed green.

Neither existing reading grows. Both are exact about a population and say so in
their own comments, so this adds a third, LinkTargetsAboveTheirDocument, which
returns a link target that steps out of the document's own directory and leaves
the caller to place it. pathsNamedIn joins it to the document's directory rather
than to the root, and drops a target that climbs past the root, because whether
a file outside the checkout exists is not a question any reading of this tree
answers.

Closes #168

Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com>
@iderex iderex added bug Something isn't working ci labels Aug 22, 2026
@iderex iderex self-assigned this Aug 22, 2026
@iderex
iderex merged commit d9876c2 into main Aug 22, 2026
25 checks passed
@iderex
iderex deleted the invariants/a-document-under-docs-linking-a-root-file branch August 22, 2026 07:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working ci

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A document under docs/ can link a root file that is not in the tree and nothing refuses it

1 participant